{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# City street network orientations\n",
    "\n",
    "Author: [Geoff Boeing](https://geoffboeing.com/)\n",
    "\n",
    "Compare the spatial orientations of city street networks with OSMnx.\n",
    "\n",
    "  - [Overview of OSMnx](http://geoffboeing.com/2016/11/osmnx-python-street-networks/)\n",
    "  - [GitHub repo](https://github.com/gboeing/osmnx)\n",
    "  - [Examples, demos, tutorials](https://github.com/gboeing/osmnx-examples)\n",
    "  - [Documentation](https://osmnx.readthedocs.io/en/stable/)\n",
    "  - [Journal article/citation](http://geoffboeing.com/publications/osmnx-complex-street-networks/)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'1.0.0'"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import datetime\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import osmnx as ox\n",
    "import pandas as pd\n",
    "%matplotlib inline\n",
    "ox.config(log_console=True)\n",
    "weight_by_length = False\n",
    "\n",
    "ox.__version__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# define the study sites as label : query\n",
    "places = {#'Atlanta'       : 'Atlanta, Georgia, USA',\n",
    "          #'Boston'        : 'Boston, MA, USA',\n",
    "           'Buffalo'       : 'Buffalo, NY, USA',\n",
    "          #'Charlotte'     : 'Charlotte, NC, USA',\n",
    "          #'Chicago'       : 'Chicago, IL, USA',\n",
    "           'Cleveland'     : 'Cleveland, OH, USA',\n",
    "          #'Dallas'        : 'Dallas, TX, USA',\n",
    "          #'Houston'       : 'Houston, TX, USA',\n",
    "          #'Denver'        : 'Denver, CO, USA',\n",
    "          #'Detroit'       : 'Detroit, MI, USA',\n",
    "          #'Las Vegas'     : 'Las Vegas, NV, USA',\n",
    "          #'Los Angeles'   : {'city':'Los Angeles', 'state':'CA', 'country':'USA'},\n",
    "          #'Manhattan'     : 'Manhattan, NYC, NY, USA',\n",
    "           'Miami'         : 'Miami, FL, USA',\n",
    "           'Minneapolis'   : 'Minneapolis, MN, USA',\n",
    "          #'Orlando'       : 'Orlando, FL, USA',\n",
    "          #'Philadelphia'  : 'Philadelphia, PA, USA',\n",
    "          #'Phoenix'       : 'Phoenix, AZ, USA',\n",
    "          #'Portland'      : 'Portland, OR, USA',\n",
    "          #'Sacramento'    : 'Sacramento, CA, USA',\n",
    "           'San Francisco' : {'city':'San Francisco', 'state':'CA', 'country':'USA'},\n",
    "          #'Seattle'       : 'Seattle, WA, USA',\n",
    "          #'St Louis'      : 'St. Louis, MO, USA',\n",
    "          #'Tampa'         : 'Tampa, FL, USA',\n",
    "           'Washington'    : 'District of Columbia, USA'}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>geometry</th>\n",
       "      <th>bbox_north</th>\n",
       "      <th>bbox_south</th>\n",
       "      <th>bbox_east</th>\n",
       "      <th>bbox_west</th>\n",
       "      <th>place_id</th>\n",
       "      <th>osm_type</th>\n",
       "      <th>osm_id</th>\n",
       "      <th>lat</th>\n",
       "      <th>lon</th>\n",
       "      <th>display_name</th>\n",
       "      <th>class</th>\n",
       "      <th>type</th>\n",
       "      <th>importance</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>POLYGON ((-78.91945 42.94717, -78.91913 42.946...</td>\n",
       "      <td>42.966469</td>\n",
       "      <td>42.826039</td>\n",
       "      <td>-78.795168</td>\n",
       "      <td>-78.919453</td>\n",
       "      <td>258446311</td>\n",
       "      <td>relation</td>\n",
       "      <td>175031</td>\n",
       "      <td>42.886717</td>\n",
       "      <td>-78.878392</td>\n",
       "      <td>Buffalo, Erie, New York, United States</td>\n",
       "      <td>boundary</td>\n",
       "      <td>administrative</td>\n",
       "      <td>0.732931</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>POLYGON ((-81.87909 41.39641, -81.87906 41.395...</td>\n",
       "      <td>41.604436</td>\n",
       "      <td>41.390628</td>\n",
       "      <td>-81.532744</td>\n",
       "      <td>-81.879094</td>\n",
       "      <td>258019775</td>\n",
       "      <td>relation</td>\n",
       "      <td>182130</td>\n",
       "      <td>41.505161</td>\n",
       "      <td>-81.693445</td>\n",
       "      <td>Cleveland, Cuyahoga County, Ohio, United States</td>\n",
       "      <td>boundary</td>\n",
       "      <td>administrative</td>\n",
       "      <td>0.856081</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>POLYGON ((-80.31976 25.76249, -80.31968 25.762...</td>\n",
       "      <td>25.855783</td>\n",
       "      <td>25.709052</td>\n",
       "      <td>-80.139157</td>\n",
       "      <td>-80.319760</td>\n",
       "      <td>258523095</td>\n",
       "      <td>relation</td>\n",
       "      <td>1216769</td>\n",
       "      <td>25.774173</td>\n",
       "      <td>-80.193620</td>\n",
       "      <td>Miami, Miami-Dade County, Florida, United States</td>\n",
       "      <td>boundary</td>\n",
       "      <td>administrative</td>\n",
       "      <td>0.885449</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>POLYGON ((-93.32916 44.94142, -93.32914 44.940...</td>\n",
       "      <td>45.051250</td>\n",
       "      <td>44.890150</td>\n",
       "      <td>-93.193858</td>\n",
       "      <td>-93.329163</td>\n",
       "      <td>297784393</td>\n",
       "      <td>relation</td>\n",
       "      <td>136712</td>\n",
       "      <td>44.977300</td>\n",
       "      <td>-93.265469</td>\n",
       "      <td>Minneapolis, Hennepin County, Minnesota, Unite...</td>\n",
       "      <td>boundary</td>\n",
       "      <td>administrative</td>\n",
       "      <td>0.747169</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>MULTIPOLYGON (((-123.17382 37.77573, -123.1737...</td>\n",
       "      <td>37.929811</td>\n",
       "      <td>37.640314</td>\n",
       "      <td>-122.281479</td>\n",
       "      <td>-123.173825</td>\n",
       "      <td>257346022</td>\n",
       "      <td>relation</td>\n",
       "      <td>111968</td>\n",
       "      <td>37.779026</td>\n",
       "      <td>-122.419906</td>\n",
       "      <td>San Francisco, California, United States</td>\n",
       "      <td>boundary</td>\n",
       "      <td>administrative</td>\n",
       "      <td>1.025131</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>POLYGON ((-77.11977 38.93428, -77.11900 38.933...</td>\n",
       "      <td>38.995852</td>\n",
       "      <td>38.791630</td>\n",
       "      <td>-76.909366</td>\n",
       "      <td>-77.119766</td>\n",
       "      <td>258375899</td>\n",
       "      <td>relation</td>\n",
       "      <td>162069</td>\n",
       "      <td>38.893794</td>\n",
       "      <td>-76.987998</td>\n",
       "      <td>Washington, D.C., United States</td>\n",
       "      <td>boundary</td>\n",
       "      <td>administrative</td>\n",
       "      <td>0.412066</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                            geometry  bbox_north  bbox_south  \\\n",
       "0  POLYGON ((-78.91945 42.94717, -78.91913 42.946...   42.966469   42.826039   \n",
       "1  POLYGON ((-81.87909 41.39641, -81.87906 41.395...   41.604436   41.390628   \n",
       "2  POLYGON ((-80.31976 25.76249, -80.31968 25.762...   25.855783   25.709052   \n",
       "3  POLYGON ((-93.32916 44.94142, -93.32914 44.940...   45.051250   44.890150   \n",
       "4  MULTIPOLYGON (((-123.17382 37.77573, -123.1737...   37.929811   37.640314   \n",
       "5  POLYGON ((-77.11977 38.93428, -77.11900 38.933...   38.995852   38.791630   \n",
       "\n",
       "    bbox_east   bbox_west   place_id  osm_type   osm_id        lat  \\\n",
       "0  -78.795168  -78.919453  258446311  relation   175031  42.886717   \n",
       "1  -81.532744  -81.879094  258019775  relation   182130  41.505161   \n",
       "2  -80.139157  -80.319760  258523095  relation  1216769  25.774173   \n",
       "3  -93.193858  -93.329163  297784393  relation   136712  44.977300   \n",
       "4 -122.281479 -123.173825  257346022  relation   111968  37.779026   \n",
       "5  -76.909366  -77.119766  258375899  relation   162069  38.893794   \n",
       "\n",
       "          lon                                       display_name     class  \\\n",
       "0  -78.878392             Buffalo, Erie, New York, United States  boundary   \n",
       "1  -81.693445    Cleveland, Cuyahoga County, Ohio, United States  boundary   \n",
       "2  -80.193620   Miami, Miami-Dade County, Florida, United States  boundary   \n",
       "3  -93.265469  Minneapolis, Hennepin County, Minnesota, Unite...  boundary   \n",
       "4 -122.419906           San Francisco, California, United States  boundary   \n",
       "5  -76.987998                    Washington, D.C., United States  boundary   \n",
       "\n",
       "             type  importance  \n",
       "0  administrative    0.732931  \n",
       "1  administrative    0.856081  \n",
       "2  administrative    0.885449  \n",
       "3  administrative    0.747169  \n",
       "4  administrative    1.025131  \n",
       "5  administrative    0.412066  "
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# verify OSMnx geocodes each query to what you expect (i.e., a [multi]polygon geometry)\n",
    "gdf = ox.geocode_to_gdf(list(places.values()))\n",
    "gdf"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Get the street networks and their edge bearings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def reverse_bearing(x):\n",
    "    return x + 180 if x < 180 else x - 180"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2020-12-23 16:44:28.440075 Buffalo\n",
      "2020-12-23 16:44:33.478631 Cleveland\n",
      "2020-12-23 16:44:43.402285 Miami\n",
      "2020-12-23 16:44:51.890193 Minneapolis\n",
      "2020-12-23 16:45:02.708641 San Francisco\n",
      "2020-12-23 16:45:17.804208 Washington\n"
     ]
    }
   ],
   "source": [
    "bearings = {}\n",
    "for place in sorted(places.keys()):\n",
    "    print(datetime.datetime.now(), place)\n",
    "    \n",
    "    # get the graph\n",
    "    query = places[place]\n",
    "    G = ox.graph_from_place(query, network_type='drive')\n",
    "    \n",
    "    # calculate edge bearings\n",
    "    Gu = ox.add_edge_bearings(ox.get_undirected(G))\n",
    "    \n",
    "    if weight_by_length:\n",
    "        # weight bearings by length (meters)\n",
    "        city_bearings = []\n",
    "        for u, v, k, d in Gu.edges(keys=True, data=True):\n",
    "            city_bearings.extend([d['bearing']] * int(d['length']))\n",
    "        b = pd.Series(city_bearings)\n",
    "        bearings[place] = pd.concat([b, b.map(reverse_bearing)]).reset_index(drop='True')\n",
    "    else:\n",
    "        # don't weight bearings, just take one value per street segment\n",
    "        b = pd.Series([d['bearing'] for u, v, k, d in Gu.edges(keys=True, data=True)])\n",
    "        bearings[place] = pd.concat([b, b.map(reverse_bearing)]).reset_index(drop='True')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Visualize it"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def count_and_merge(n, bearings):\n",
    "    # make twice as many bins as desired, then merge them in pairs\n",
    "    # prevents bin-edge effects around common values like 0° and 90°\n",
    "    n = n * 2\n",
    "    bins = np.arange(n + 1) * 360 / n\n",
    "    count, _ = np.histogram(bearings, bins=bins)\n",
    "    \n",
    "    # move the last bin to the front, so eg 0.01° and 359.99° will be binned together\n",
    "    count = np.roll(count, 1)\n",
    "    return count[::2] + count[1::2]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "# function to draw a polar histogram for a set of edge bearings\n",
    "def polar_plot(ax, bearings, n=36, title=''):\n",
    "\n",
    "    bins = np.arange(n + 1) * 360 / n\n",
    "    count = count_and_merge(n, bearings)\n",
    "    _, division = np.histogram(bearings, bins=bins)\n",
    "    frequency = count / count.sum()\n",
    "    division = division[0:-1]\n",
    "    width =  2 * np.pi / n\n",
    "\n",
    "    ax.set_theta_zero_location('N')\n",
    "    ax.set_theta_direction('clockwise')\n",
    "\n",
    "    x = division * np.pi / 180\n",
    "    bars = ax.bar(x, height=frequency, width=width, align='center', bottom=0, zorder=2,\n",
    "                  color='#003366', edgecolor='k', linewidth=0.5, alpha=0.7)\n",
    "    \n",
    "    ax.set_ylim(top=frequency.max())\n",
    "    \n",
    "    title_font = {'family':'DejaVu Sans', 'size':24, 'weight':'bold'}\n",
    "    xtick_font = {'family':'DejaVu Sans', 'size':10, 'weight':'bold', 'alpha':1.0, 'zorder':3}\n",
    "    ytick_font = {'family':'DejaVu Sans', 'size': 9, 'weight':'bold', 'alpha':0.2, 'zorder':3}\n",
    "    \n",
    "    ax.set_title(title.upper(), y=1.05, fontdict=title_font)\n",
    "    \n",
    "    ax.set_yticks(np.linspace(0, max(ax.get_ylim()), 5))\n",
    "    yticklabels = ['{:.2f}'.format(y) for y in ax.get_yticks()]\n",
    "    yticklabels[0] = ''\n",
    "    ax.set_yticklabels(labels=yticklabels, fontdict=ytick_font)\n",
    "    \n",
    "    xticklabels = ['N', '', 'E', '', 'S', '', 'W', '']\n",
    "    ax.set_xticks(ax.get_xticks())\n",
    "    ax.set_xticklabels(labels=xticklabels, fontdict=xtick_font)\n",
    "    ax.tick_params(axis='x', which='major', pad=-2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "# create figure and axes\n",
    "n = len(places)\n",
    "ncols = int(np.ceil(np.sqrt(n)))\n",
    "nrows = int(np.ceil(n / ncols))\n",
    "figsize = (ncols * 5, nrows * 5)\n",
    "fig, axes = plt.subplots(nrows, ncols, figsize=figsize, subplot_kw={'projection':'polar'})\n",
    "\n",
    "# plot each city's polar histogram\n",
    "for ax, place in zip(axes.flat, sorted(places.keys())):\n",
    "    polar_plot(ax, bearings[place].dropna(), title=place)\n",
    "\n",
    "# add super title and save full image\n",
    "suptitle_font = {'family':'DejaVu Sans', 'fontsize':60, 'fontweight':'normal', 'y':1.07}\n",
    "fig.suptitle('City Street Network Orientation', **suptitle_font)\n",
    "fig.tight_layout()\n",
    "fig.subplots_adjust(hspace=0.35)\n",
    "fig.savefig('images/street-orientations.png', dpi=120, bbox_inches='tight')\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python (ox)",
   "language": "python",
   "name": "ox"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
